package dkbta.processing;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import dkbta.Config;
import dkbta.ontology.Ontology;
import dkbta.ontology.elements.Element;
import dkbta.ontology.elements.ElementDef;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.elements.event.EventDef;
import dkbta.ontology.elements.primitive.PrimitiveDef;
import dkbta.ontology.elements.stat.Stat;
import dkbta.processing.data.DB;
import dkbta.processing.data.DBDataReader;
import dkbta.processing.data.DataBatch;
import dkbta.processing.data.DataDispatcher;
import dkbta.processing.data.InputStreamDataReader;
import dkbta.processing.data.SendTo;
import dkbta.util.Pair;

/**
 * A container of all the patients in the system. <br>
 * Also holds all of the elements that are not associated with a single patient (e.g.
 * statistical abstractions)
 * 
 * @author Uri Kanonov
 */
public class Patients implements Iterable<Patient>{
	/** A mapping from patient IDs to their respective patients. Sorted by patient ID */
	private final SortedMap<Integer, Patient> _patients;

	/** The data dispatcher, used to send instances to the DB upon computation finish */
	private final DataDispatcher _dd;

	/** A mapping from statistical abstraction name to its respective instances */
	private final Map<String, List<Stat>> _stats;

	/** The send-to information per all multiple-patient elements */
	private Map<ElementDef, SendTo> _sendTos;

	public Patients(DataDispatcher dataDispatcher){
		_dd = dataDispatcher;
		_patients = new TreeMap<Integer, Patient>();
		_stats = new HashMap<String, List<Stat>>();
	}

	/**
	 * Looks up a patient based on its id
	 * 
	 * @param patientId The patient's id
	 * @return The patient if found
	 * @throws UnknownPatientException If the patient could not be found
	 */
	public Patient getPatient(int patientId) throws UnknownPatientException{
		Patient patient = _patients.get(patientId);
		if (patient == null){
			throw new UnknownPatientException(patientId);
		}
		return patient;
	}

	/**
	 * Adds a patient to the container. This should be called only during the
	 * initialization phase
	 * 
	 * @param p The patient to be added
	 */
	public void addPatient(Patient p){
		_patients.put(p.getPatientId(), p);
	}

	/**
	 * Returns an iterator over the patients in this container
	 * 
	 * @return An iterator over the patients in this container
	 */
	@Override
	public Iterator<Patient> iterator(){
		return _patients.values().iterator();
	}

	/**
	 * Returns an iterator over the patients in this container that are in the given range
	 * of its
	 * 
	 * @param patientIdRanges Ranges of patient ids to be iterated over
	 * @return An iterator over the patients in this container that are in the given range
	 *         of its
	 * @throws UnknownPatientException If an id in the range refers to a patient not in
	 *         the container
	 */
	public Iterator<Patient> rangeIterator(Pair<Integer, Integer>[] patientIdRanges){
		return new RangeIterator<Patient>(patientIdRanges, _patients, false);
	}

	/**
	 * Retrieves the instances of the statistical abstractions
	 * 
	 * @param name The statistical abstraction's name
	 * @return The list of instances
	 */
	public List<Stat> getStats(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _stats);
	}

	/**
	 * Returns the list of instances for the specified element
	 * 
	 * @param name The element name
	 * @param elementsByName All of the instance's of the element's type
	 */
	private <T extends Element> List<T> get(String name,
		Map<String, List<T>> elementsByName){
		List<T> elements = elementsByName.get(name);

		if (elements == null){
			elements = new ArrayList<T>();
			elementsByName.put(name, elements);
		}
		return elements;
	}

	/**
	 * Retrieves the instances of an element according to the name and type
	 * 
	 * @param elementType The element's type
	 * @param elementName The element's name
	 * @return The list of instances or null if the name is null, eType is null or the
	 *         element has no instances
	 */
	public List<? extends Element> getElements(ElementType elementType, String elementName){
		if (elementType == null || elementName == null){
			throw new NullPointerException("Element type and element name cannot be null");
		}else{
			switch (elementType){
				case STAT:
					return getStats(elementName);
				default:
					throw new UnsupportedOperationException("Getter not implemented for "
							+ elementType);
			}
		}
	}

	/**
	 * Initializes the primitives and events of all patients from the given file
	 * 
	 * @param ontology Ontology reference
	 * @param file The file containing the data
	 * @throws Exception If the initialization fails
	 */
	public void initializeFromFile(Ontology ontology, String file) throws Exception{
		ActionTimer actionTimer = ActionTimer.getInstance();
		int dataReadingActionId = actionTimer.actionStart();

		InputStream is = new FileInputStream(file);
		new InputStreamDataReader(ontology, is, this, false,
				InputStreamDataReader.ValidElements.REQUIRED_PRIMITIVES_AND_EVENTS)
				.read();

		actionTimer.actionEnd(dataReadingActionId, -1, null, null,
			ActionTimer.Action.READING_DATA_FROM_FILE);
		setAllPrimitivesAndEventsAsComputed(ontology);
	}

	/**
	 * Initializes the primitives and events of all patients from the database
	 * 
	 * @param ontology Ontology reference
	 * @param db Database reference
	 * @param allAtOnceQuery The query to be used in a "reading-all-at-once" mode
	 * @throws Exception If the initialization fails
	 */
	public void initializeFromDB(Ontology ontology, DB db, String allAtOnceQuery)
			throws Exception{
		ActionTimer actionTimer = ActionTimer.getInstance();

		int dataReadingActionId = actionTimer.actionStart();
		new DBDataReader(db, this, allAtOnceQuery).read();
		actionTimer.actionEnd(dataReadingActionId, -1, null, null,
			ActionTimer.Action.READING_DATA_FROM_DB);

		setAllPrimitivesAndEventsAsComputed(ontology);
	}

	/**
	 * Sets all the primitives and events of all patients as computed so that no
	 * computation tasks for these elements will be created. <br>
	 * This should be called at the end of the initialization process.
	 * 
	 * @param ontology Ontology reference
	 */
	private void setAllPrimitivesAndEventsAsComputed(Ontology ontology){
		// Setting all primitives and events for all patients as computed
		Collection<PrimitiveDef> primitives = ontology.getAllPrimitives();
		Collection<EventDef> events = ontology.getAllEvents();
		for (Patient p : _patients.values()){
			for (PrimitiveDef pd : primitives){
				p.addComputedElement(pd);
			}
			for (ElementDef ed : events){
				p.addComputedElement(ed);
			}
		}
	}

	/**
	 * Notes that the given multi-patient element has been computed
	 * 
	 * @param element The element that has been computed
	 */
	public void addComputedElement(ElementDef element){
		// Checking if the computed data was to be sent to another unit
		SendTo sendTo = _sendTos.get(element);
		if (sendTo != null){
			_dd.addDataBatch(new DataBatch(element, sendTo, getElements(element
					.getElementType(), element.getName()), -1));
		}
	}

	/**
	 * Sets the send-to information per all multiple-patient elements
	 * 
	 * @param sendTos The send-to information per all multiple-patient elements
	 */
	public void setSendTos(Map<ElementDef, SendTo> sendTos){
		_sendTos = sendTos;
	}

	/**
	 * An iterator over the patients considering ranges of ids
	 * 
	 * @author Uri Kanonov
	 */
	private static class RangeIterator<T> implements Iterator<T>{
		private final Map<Integer, T> _m;

		private final Pair<Integer, Integer>[] _ranges;

		private int _currentRangeIndex;

		private Pair<Integer, Integer> _currentRange;

		private int _nextId;

		private T _nextPatient;

		private final boolean _ignoreMissingPatients;

		public RangeIterator(Pair<Integer, Integer>[] ranges, Map<Integer, T> m,
			boolean ignoreMissingPatients){
			_ranges = ranges;
			_m = m;
			_ignoreMissingPatients = ignoreMissingPatients;
			_currentRangeIndex = 0;
			_currentRange = ranges[_currentRangeIndex];
			_nextId = _currentRange.getFirst();
			advance();
		}

		@Override
		public boolean hasNext(){
			return _nextPatient != null;
		}

		@Override
		public T next(){
			T value = _nextPatient;
			advance();
			return value;
		}

		private void advance(){
			if (_nextId > _currentRange.getSecond()){
				if (++_currentRangeIndex < _ranges.length){
					_currentRange = _ranges[_currentRangeIndex];
					_nextId = _currentRange.getFirst();
				}else{ // We've finished traversing the ranges
					_nextPatient = null;
					return;
				}
			}

			if ((_nextPatient = _m.get(_nextId++)) == null){
				if (_ignoreMissingPatients){
					advance();
				}else{
					throw new UnknownPatientException(_nextId - 1);
				}
			}
		}

		@Override
		public void remove(){
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * Whether elements with no instances should be printed in summary mode with a "- 0"
	 * or not a all
	 */
	private static final boolean PRINT_EMPTY_ELEMENTS = Config.getBoolean(
		"PRINT_EMPTY_ELEMENTS", true);

	/** Whether the primitives should be summarized */
	private static final boolean SUMMARIZE_PRIMITIVES = Config.getBoolean(
		"SUMMARIZE_PRIMITIVES", true);

	/** Whether the events should be summarized */
	private static final boolean SUMMARIZE_EVENTS = Config.getBoolean("SUMMARIZE_EVENTS",
		true);

	/** Whether the contexts should be summarized */
	private static final boolean SUMMARIZE_CONTEXTS = Config.getBoolean(
		"SUMMARIZE_CONTEXTS", true);

	/** Whether the states should be summarized */
	private static final boolean SUMMARIZE_STATES = Config.getBoolean("SUMMARIZE_STATES",
		true);

	/** Whether the gradients should be summarized */
	private static final boolean SUMMARIZE_GRADIENTS = Config.getBoolean(
		"SUMMARIZE_GRADIENTS", true);

	/** Whether the rates should be summarized */
	private static final boolean SUMMARIZE_RATES = Config.getBoolean("SUMMARIZE_RATES",
		true);

	/** Whether the trends should be summarized */
	private static final boolean SUMMARIZE_TRENDS = Config.getBoolean("SUMMARIZE_TRENDS",
		true);

	/** Whether the patterns should be summarized */
	private static final boolean SUMMARIZE_PATTERNS = Config.getBoolean(
		"SUMMARIZE_PATTERNS", true);

	/** Whether the repeating patterns should be summarized */
	private static final boolean SUMMARIZE_REPEATING_PATTERNS = Config.getBoolean(
		"SUMMARIZE_REPEATING_PATTERNS", true);

	/** Whether the subject-statistical abstractions should be summarized */
	private static final boolean SUMMARIZE_SUBJECT_STATS = Config.getBoolean(
		"SUMMARIZE_SUBJECT_STATS", true);

	/** Whether the statistical abstractions should be summarized */
	private static final boolean SUMMARIZE_STATS = Config.getBoolean("SUMMARIZE_STATS",
		true);

	/**
	 * Prints all of the instances of all of the patients and the multi-patient ones
	 * 
	 * @param detailedElements A list of elements that should be detailed (not summarized)
	 *        even if their type should be summarized in general
	 */
	public void printInstances(String... detailedElements){
		Set<String> detailedElementSet = new HashSet<String>(Arrays
				.asList(detailedElements));
		for (Patient p : this){
			System.out.println("---- Patient #" + p.getPatientId() + "----");
			p.printInstances(SUMMARIZE_PRIMITIVES, SUMMARIZE_EVENTS, SUMMARIZE_CONTEXTS,
				SUMMARIZE_STATES, SUMMARIZE_GRADIENTS, SUMMARIZE_RATES, SUMMARIZE_TRENDS,
				SUMMARIZE_PATTERNS, SUMMARIZE_REPEATING_PATTERNS,
				SUMMARIZE_SUBJECT_STATS, detailedElementSet);
			System.out.println();
		}

		System.out.println("---- Statistical Abstractions ----");
		print(_stats, SUMMARIZE_STATS, "Stats", detailedElementSet);
	}

	static <T extends Element> void print(Map<String, List<T>> elementsByName,
		boolean summary, String type, Set<String> detailedElements){
		if (elementsByName.isEmpty()){
			return;
		}
		System.out.println(type);
		for (Map.Entry<String, List<T>> nameAndElements : elementsByName.entrySet()){
			List<T> elements = nameAndElements.getValue();
			String elementName = nameAndElements.getKey();
			if (summary && !detailedElements.contains(elementName)){
				int numElements = elements.size();
				if (PRINT_EMPTY_ELEMENTS || numElements > 0){
					System.out.println("\t" + elementName + " - " + numElements);
				}
			}else{
				Collections.sort(elements);
				for (Element e : elements){
					System.out.print("\t");
					System.out.println(e);
				}
			}
		}
	}
}
